/**
 * ***************************************************************************** Copyright (c) 2003,
 * 2006 Subclipse project and others. All rights reserved. This program and the accompanying
 * materials are made available under the terms of the Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * <p>Contributors: Subclipse project committers - initial API and implementation
 * ****************************************************************************
 */
package org.tigris.subversion.subclipse.core.repo;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Properties;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.team.core.TeamException;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
import org.tigris.subversion.subclipse.core.ISVNRemoteFile;
import org.tigris.subversion.subclipse.core.ISVNRemoteFolder;
import org.tigris.subversion.subclipse.core.ISVNRemoteResource;
import org.tigris.subversion.subclipse.core.ISVNRepositoryLocation;
import org.tigris.subversion.subclipse.core.Policy;
import org.tigris.subversion.subclipse.core.SVNException;
import org.tigris.subversion.subclipse.core.SVNProviderPlugin;
import org.tigris.subversion.subclipse.core.SVNStatus;
import org.tigris.subversion.subclipse.core.client.NotificationListener;
import org.tigris.subversion.subclipse.core.resources.RemoteFile;
import org.tigris.subversion.subclipse.core.resources.RemoteFolder;
import org.tigris.subversion.svnclientadapter.ISVNClientAdapter;
import org.tigris.subversion.svnclientadapter.ISVNInfo;
import org.tigris.subversion.svnclientadapter.SVNClientException;
import org.tigris.subversion.svnclientadapter.SVNRevision;
import org.tigris.subversion.svnclientadapter.SVNUrl;

/**
 * This class manages a SVN repository location. <br>
 * After modifying a SVNRepositoryLocation (label, username, password), add it to repositories using
 * {@link SVNRepositories#addOrUpdateRepository(ISVNRepositoryLocation)}
 */
public class SVNRepositoryLocation implements ISVNRepositoryLocation, IUserInfo, IAdaptable {

  /**
   * The name of the preferences node in the CVS preferences that contains the known repositories as
   * its children.
   */
  public static final String PREF_REPOSITORIES_NODE = "repositories"; // $NON-NLS-1$

  /*
   * The name of the node in the default scope that has the default settings
   * for a repository.
   */
  private static final String DEFAULT_REPOSITORY_SETTINGS_NODE =
      "default_repository_settings"; //$NON-NLS-1$

  // Preference keys used to persist the state of the location
  public static final String PREF_LOCATION = "location"; // $NON-NLS-1$
  public static final String PREF_SERVER_ENCODING = "encoding"; // $NON-NLS-1$

  // friendly name of the location
  private String label = null;

  private String user;

  // url of this location
  private SVNUrl url;

  // url of the root repository
  private SVNUrl repositoryRootUrl;

  // the folder corresponding to this repository location
  private RemoteFolder rootFolder;

  // fields needed for caching the password
  public static final String INFO_PASSWORD =
      "org.tigris.subversion.subclipse.core.password"; //$NON-NLS-1$
  public static final String INFO_USERNAME =
      "org.tigris.subversion.subclipse.core.username"; //$NON-NLS-1$
  public static final String AUTH_SCHEME = ""; // $NON-NLS-1$
  public static final URL FAKE_URL;

  public static final String USER_VARIABLE = "{user}"; // $NON-NLS-1$
  public static final String PASSWORD_VARIABLE = "{password}"; // $NON-NLS-1$
  public static final String HOST_VARIABLE = "{host}"; // $NON-NLS-1$
  public static final String PORT_VARIABLE = "{port}"; // $NON-NLS-1$

  //	private ISVNClientAdapter svnClient;

  static {
    URL temp = null;
    try {
      temp = new URL("http://org.tigris.subversion.subclipse.core"); // $NON-NLS-1$
    } catch (MalformedURLException e) {
    }
    // The protection space is defined by this url and realm (AUTH_SCHEME)
    FAKE_URL = temp;
  }

  /**
   * Return the preferences node whose child nodes are the know repositories
   *
   * @return a preferences node
   */
  public static Preferences getParentPreferences() {
    return SVNProviderPlugin.getPlugin().getInstancePreferences().node(PREF_REPOSITORIES_NODE);
  }

  /**
   * Return a preferences node that contains suitabel defaults for a repository location.
   *
   * @return a preferences node
   */
  public static Preferences getDefaultPreferences() {
    Preferences defaults =
        new DefaultScope().getNode(SVNProviderPlugin.ID).node(DEFAULT_REPOSITORY_SETTINGS_NODE);
    defaults.put(PREF_SERVER_ENCODING, getDefaultEncoding());
    return defaults;
  }

  private static String getDefaultEncoding() {
    return System.getProperty("file.encoding", "UTF-8"); // $NON-NLS-1$ //$NON-NLS-2$
  }
  /*
   * Create a SVNRepositoryLocation from its composite parts.
   */
  private SVNRepositoryLocation(
      String user, String password, SVNUrl url, SVNUrl repositoryRootUrl) {
    this.user = user;
    this.url = url;
    this.repositoryRootUrl = repositoryRootUrl;

    rootFolder = new RemoteFolder(this, url, SVNRevision.HEAD);
  }

  /*
   * Dispose of the receiver by clearing any cached authorization information.
   * This method should only be invoked when the corresponding adapter is shut
   * down or a connection is being validated.
   */
  public void dispose() throws SVNException {
    ensurePreferencesStored();
    // remove repo location from preferences
    try {
      if (hasPreferences()) {
        internalGetPreferences().removeNode();
        getParentPreferences().flush();
      }
    } catch (BackingStoreException e) {
      SVNProviderPlugin.log(SVNException.wrapException(e));
    }
  }

  /*
   * @see ISVNRepositoryLocation#getUrl()
   */
  public SVNUrl getUrl() {
    return url;
  }

  /*
   * @see IRepositoryLocation#getLocation()
   */
  public String getLocation() {
    return getUrl().toString();
  }

  public ISVNRemoteFolder getRootFolder() {
    //        // refresh it so that members don't return always the same remote resources ...
    //        rootFolder.refresh();
    return rootFolder;
  }

  public void refreshRootFolder() {
    rootFolder.refresh();
  }

  /*
   * @see ISVNRepositoryLocation#members(IProgressMonitor)
   */
  public ISVNRemoteResource[] members(IProgressMonitor progress) throws SVNException {
    try {
      ISVNRemoteResource[] resources = getRootFolder().members(progress);
      return resources;
    } catch (TeamException e) {
      throw new SVNException(e.getStatus());
    }
  }

  /*
   * (non-Javadoc)
   * @see org.tigris.subversion.subclipse.core.ISVNRepositoryLocation#getRemoteFolder(java.lang.String)
   */
  public ISVNRemoteFolder getRemoteFolder(String remotePath) {
    return new RemoteFolder(this, getUrl().appendPath(remotePath), SVNRevision.HEAD);
  }

  /*
   * (non-Javadoc)
   * @see org.tigris.subversion.subclipse.core.ISVNRepositoryLocation#getRemoteFile(org.tigris.subversion.svnclientadapter.SVNUrl)
   */
  public ISVNRemoteFile getRemoteFile(SVNUrl url) throws SVNException {
    ISVNClientAdapter svnClient = getSVNClient();
    ISVNInfo info = null;
    try {
      if (this.getRepositoryRoot().equals(url)) return new RemoteFile(this, url, SVNRevision.HEAD);
      else info = svnClient.getInfo(url, SVNRevision.HEAD, SVNRevision.HEAD);
    } catch (SVNClientException e) {
      throw new SVNException("Can't get latest remote resource for " + url);
    }

    if (info == null) return null; // no remote file
    else {
      return new RemoteFile(
          null, // we don't know its parent
          this,
          url,
          SVNRevision.HEAD,
          info.getLastChangedRevision(),
          info.getLastChangedDate(),
          info.getLastCommitAuthor());
    }
  }

  public ISVNRemoteFile getRemoteFile(String remotePath) throws SVNException {
    return getRemoteFile(getUrl().appendPath(remotePath));
  }

  /*
   * @see ISVNRepositoryLocation#getUsername()
   * @see IUserInfo#getUsername()
   */
  public String getUsername() {
    return user == null ? "" : user; // $NON-NLS-1$
  }

  /**
   * get the svn client corresponding to the repository
   *
   * @throws SVNException
   */
  public ISVNClientAdapter getSVNClient() throws SVNException {
    ISVNClientAdapter svnClient = SVNProviderPlugin.getPlugin().getSVNClient();

    svnClient.addNotifyListener(NotificationListener.getInstance());

    return svnClient;
  }

  public void returnSVNClient(ISVNClientAdapter client) {
    SVNProviderPlugin.getPlugin().getSVNClientManager().returnSVNClient(client);
  }

  /*
   * Implementation of inherited toString()
   */
  public String toString() {
    if (getLabel() != null) {
      return getLabel();
    } else {
      return getLocation();
    }
  }

  public boolean equals(Object o) {
    if (!(o instanceof SVNRepositoryLocation)) return false;
    return getLocation().equals(((SVNRepositoryLocation) o).getLocation());
  }

  public int hashCode() {
    return getLocation().hashCode();
  }

  /*
   * @see IUserInfo#setPassword(String)
   */
  public void setPassword(String password) {
    // We stopped storing passwords prior to 1.0
    // just ignore this call
  }

  /*
   * @see IUserInfo#setUsername(String)
   */
  public void setUsername(String user) {
    // We stopped storing passwords prior to 1.0
    // just ignore this call
  }

  /** add user and password to the keyring */
  public void updateCache() throws SVNException {
    ensurePreferencesStored();
    // We stopped storing passwords prior to 1.0
    // just ignore this call
  }

  /*
   * Validate that the receiver contains valid information for
   * making a connection. If the receiver contains valid
   * information, the method returns. Otherwise, an exception
   * indicating the problem is throw.
   */
  public void validateConnection(IProgressMonitor monitor) throws SVNException {
    ISVNClientAdapter svnClient = getSVNClient();
    try {
      // we try to get the list of directories and files using the connection
      ISVNInfo info = svnClient.getInfo(getUrl());
      repositoryRootUrl = info.getRepository();
    } catch (SVNClientException e) {
      // If the validation failed, dispose of any cached info
      dispose();
      throw SVNException.wrapException(e);
    }
  }

  /*
   *  this should be made more robust --mml 11/27/03
   * @see org.tigris.subversion.subclipse.core.ISVNRepositoryLocation#pathExists()
   */
  public boolean pathExists() throws SVNException {
    ISVNClientAdapter svnClient = getSVNClient();
    try {
      svnClient.getList(getUrl(), SVNRevision.HEAD, false);
    } catch (SVNClientException e) {
      return false;
    }
    return true;
  }

  /*
   * Create a repository location instance from the given properties.
   * The supported properties are:
   *   user The username for the connection (optional)
   *   password The password used for the connection (optional)
   *   url The url where the repository resides
   *   rootUrl The repository root url
   */
  public static SVNRepositoryLocation fromProperties(Properties configuration) throws SVNException {
    // We build a string to allow validation of the components that are provided to us

    String user = configuration.getProperty("user"); // $NON-NLS-1$
    if ((user == null) || (user.length() == 0)) user = null;
    String password = configuration.getProperty("password"); // $NON-NLS-1$
    if (user == null) password = null;
    String rootUrl = configuration.getProperty("rootUrl"); // $NON-NLS-1$
    if ((rootUrl == null) || (rootUrl.length() == 0)) rootUrl = null;
    String url = configuration.getProperty("url"); // $NON-NLS-1$
    if (url == null)
      throw new SVNException(
          new Status(
              IStatus.ERROR,
              SVNProviderPlugin.ID,
              TeamException.UNABLE,
              Policy.bind("SVNRepositoryLocation.hostRequired"),
              null)); //$NON-NLS-1$

    SVNUrl urlURL = null;
    try {
      urlURL = new SVNUrl(url);
    } catch (MalformedURLException e) {
      throw new SVNException(e.getMessage());
    }

    SVNUrl rootUrlURL = null;
    if (rootUrl != null) {
      try {
        rootUrlURL = new SVNUrl(rootUrl);
      } catch (MalformedURLException e) {
        throw new SVNException(e.getMessage());
      }
    }

    return new SVNRepositoryLocation(user, password, urlURL, rootUrlURL);
  }

  /*
   * Parse a location string and return a SVNRepositoryLocation.
   *
   * On failure, the status of the exception will be a MultiStatus
   * that includes the original parsing error and a general status
   * displaying the passed location and proper form. This form is
   * better for logging, etc.
   */
  public static SVNRepositoryLocation fromString(String location) throws SVNException {
    try {
      return fromString(location, false);
    } catch (SVNException e) {
      // Parsing failed. Include a status that
      // shows the passed location and the proper form
      MultiStatus error =
          new MultiStatus(
              SVNProviderPlugin.ID,
              SVNStatus.ERROR,
              Policy.bind("SVNRepositoryLocation.invalidFormat", new Object[] {location}),
              null); //$NON-NLS-1$
      error.merge(
          new SVNStatus(
              SVNStatus.ERROR, Policy.bind("SVNRepositoryLocation.locationForm"))); // $NON-NLS-1$
      error.merge(e.getStatus());
      throw new SVNException(error);
    }
  }

  /*
   * Parse a location string and return a SVNRepositoryLocation.
   *
   * The location is an url
   */
  public static SVNRepositoryLocation fromString(String location, boolean validateOnly)
      throws SVNException {

    String partId = null;
    try {
      String user = null;
      String password = null;
      SVNUrl rootUrl = null;
      SVNUrl url = new SVNUrl(location);

      if (validateOnly)
        throw new SVNException(new SVNStatus(SVNStatus.OK, Policy.bind("ok"))); // $NON-NLS-1$

      return new SVNRepositoryLocation(user, password, url, rootUrl);
    } catch (MalformedURLException e) {
      throw new SVNException(Policy.bind(partId));
    } catch (IndexOutOfBoundsException e) {
      // We'll get here if anything funny happened while extracting substrings
      throw new SVNException(Policy.bind(partId));
    } catch (NumberFormatException e) {
      // We'll get here if we couldn't parse a number
      throw new SVNException(Policy.bind(partId));
    }
  }

  /*
   * Parse a location string and return a SVNRepositoryLocation.
   * If useRootUrl is true, use the repository root URL.
   */
  public static SVNRepositoryLocation fromString(
      String location, boolean validateOnly, boolean useRootUrl) throws SVNException {
    if (!useRootUrl) return fromString(location, validateOnly);
    ISVNClientAdapter svnClient = SVNProviderPlugin.getPlugin().getSVNClient();
    try {
      SVNUrl url = new SVNUrl(location);
      ISVNInfo info = svnClient.getInfo(url);
      SVNUrl rootUrl = info.getRepository();
      return fromString(rootUrl.toString());
    } catch (MalformedURLException e) {
      throw SVNException.wrapException(e);
    } catch (SVNClientException e) {
      throw SVNException.wrapException(e);
    } finally {
      SVNProviderPlugin.getPlugin().getSVNClientManager().returnSVNClient(svnClient);
    }
  }

  //	public static IUserAuthenticator getAuthenticator() {
  //		if (authenticator == null) {
  //			authenticator = getPluggedInAuthenticator();
  //		}
  //		return authenticator;
  //	}

  /*
   * Return the connection method registered for the given name or null if none
   * are registered
   */
  //	private static IConnectionMethod getPluggedInConnectionMethod(String methodName) {
  //		IConnectionMethod[] methods = getPluggedInConnectionMethods();
  //		for(int i=0; i<methods.length; i++) {
  //			if(methodName.equals(methods[i].getName()))
  //				return methods[i];
  //		}
  //		return null;
  //	}

  //	private static IUserAuthenticator getPluggedInAuthenticator() {
  //		IExtension[] extensions = Platform.getPluginRegistry().getExtensionPoint(CVSProviderPlugin.ID,
  // CVSProviderPlugin.PT_AUTHENTICATOR).getExtensions();
  //		if (extensions.length == 0)
  //			return null;
  //		IExtension extension = extensions[0];
  //		IConfigurationElement[] configs = extension.getConfigurationElements();
  //		if (configs.length == 0) {
  //			CVSProviderPlugin.log(new Status(IStatus.ERROR, CVSProviderPlugin.ID, 0,
  // Policy.bind("CVSAdapter.noConfigurationElement", new Object[]
  // {extension.getUniqueIdentifier()}), null));//$NON-NLS-1$
  //			return null;
  //		}
  //		try {
  //			IConfigurationElement config = configs[0];
  //			return (IUserAuthenticator) config.createExecutableExtension("run");//$NON-NLS-1$
  //		} catch (CoreException ex) {
  //			CVSProviderPlugin.log(new Status(IStatus.ERROR, CVSProviderPlugin.ID, 0,
  // Policy.bind("CVSAdapter.unableToInstantiate", new Object[] {extension.getUniqueIdentifier()}),
  // ex));//$NON-NLS-1$
  //			return null;
  //		}
  //	}

  public Object getAdapter(Class adapter) {
    if (adapter == ISVNRemoteFolder.class) return rootFolder;
    else return Platform.getAdapterManager().getAdapter(this, adapter);
  }

  /**
   * get the url of the repository root <br>
   * Ex : if url is http://svn.collab.net/viewcvs/svn/trunk/subversion/, the repository root is
   * http://svn.collab.net/viewcvs/svn
   *
   * @return
   */
  public SVNUrl getRepositoryRoot() {
    // for now, we can't get it using svn, so user must give it
    // if the user did not provide a value, we will just return
    // the repository URL.
    if (repositoryRootUrl == null) return getUrl();
    else return repositoryRootUrl;
  }

  /* (non-Javadoc)
   * @see org.tigris.subversion.subclipse.core.ISVNRepositoryLocation#setRepositoryRoot(org.tigris.subversion.svnclientadapter.SVNUrl)
   */
  public void setRepositoryRoot(SVNUrl url) {
    repositoryRootUrl = url;
  }

  /** @return Returns the label. */
  public String getLabel() {
    return label;
  }

  /** @param label The label to set. */
  public void setLabel(String label) {
    this.label = label;
  }
  /*
   * Return the preferences node for this repository
   */
  public Preferences getPreferences() {
    if (!hasPreferences()) {
      ensurePreferencesStored();
    }
    return internalGetPreferences();
  }

  private Preferences internalGetPreferences() {
    return getParentPreferences().node(getPreferenceName());
  }

  private boolean hasPreferences() {
    try {
      return getParentPreferences().nodeExists(getPreferenceName());
    } catch (Exception e) {
      // FIXME: commented 2 lines below is how CVS did it. Did i do it right?
      // CVSProviderPlugin.log(IStatus.ERROR, NLS.bind(CVSMessages.CVSRepositoryLocation_74, new
      // String[] { getLocation(true) }), e);
      // return false;
      SVNProviderPlugin.log(SVNException.wrapException(e));
      return false;
    }
  }

  /**
   * Return a unique name that identifies this location but does not contain any slashes (/). Also,
   * do not use ':'. Although a valid path character, the initial core implementation didn't handle
   * it well.
   */
  private String getPreferenceName() {
    return getLocation().replace('/', '%').replace(':', '%');
  }

  public void storePreferences() {
    Preferences prefs = internalGetPreferences();
    // Must store at least one preference in the node
    prefs.put(PREF_LOCATION, getLocation());
    flushPreferences();
  }

  private void flushPreferences() {
    try {
      internalGetPreferences().flush();
    } catch (BackingStoreException e) {
      SVNProviderPlugin.log(SVNException.wrapException(e));
    }
  }

  public void setUrl(SVNUrl url) {
    this.url = url;
  }

  private void ensurePreferencesStored() {
    if (!hasPreferences()) {
      storePreferences();
    }
  }
}
